/********************************************
 * This utils is a port of the org.gbif.portal.util.geospatial.CellIdUtils class
 * The original code is from Tim Robertson and has been ported by Javier de la Torre
 * for biodiversityatlas
 * *****************************************/
 
package net.biodiversityatlas.utils
{
	import com.afcomponents.umap.overlays.Polygon;
	import com.afcomponents.umap.types.LatLng;
	import com.afcomponents.umap.types.LatLngBounds;
	import com.biodiversityatlas.client.modelLocators.ModelLocator;
	
	/**
	 * Utilities for dealing with CellId
	 * @author tim
	 */
	public class CellIdUtils
	{
		
		/**
		 * Determines the cell id for the Lat / Long provided
		 * 
		 * @param latitude Which may be null
		 * @param longitude Which may be null
		 * 
		 * @return The cell id for the Lat Long pair
		 * 
		 */
		public static function toCellId(latitude:Number, longitude:Number):Number {
			if (latitude < -90
					|| latitude > 90
					|| longitude < -180
					|| longitude > 180) {
				throw new Error("Latitude["+ latitude+"], Longitude["+longitude+"] cannot be " +
						"converted to a cell id");
			} else {
				var la:Number = new Number(Math.floor(latitude + 90));
				var lo:Number = new Number(Math.floor(longitude + 180));
				var cellId:Number = (la * 360) + lo;
				return cellId;
			}
		}
		
		/**
		 * Returns true if the supplied cell id lies in the bounding box demarcated by the min and max cell ids supplied.
		 * @param cellId
		 * @param minMaxCellIds
		 * @return
		 * @throws Error
		 */
		public static function isCellIdInBoundingBox(cellId:int, minCellId:int, maxCellId:int):Boolean {
			if(cellId>=minCellId 
			   && cellId<=(maxCellId-361)
			   && (cellId%360) >= (minCellId %360)
			   && (cellId%360) <= ((maxCellId-361) %360))
				return true;
			return false;
		}
	
		/**
		 * Determines the centi cell id for the given values
		 * @param latitude Which may be null
		 * @param longitude Which may be null 
		 * @return The centi cell id within the cell for the lat long
		 * @throws UnableToGenerateCellIdException Shoudl the lat long be null or invalid
		 */
		public static function toCentiCellId(latitude:Number, longitude:Number):int {
			if (latitude < -90
					|| latitude > 90
					|| longitude < -180
					|| longitude > 180) {
				throw new Error("Latitude["+ latitude+"], Longitude["+longitude+"] cannot be " +
						"converted to a centi cell id");
			} else {
				
				//get decimal value for up to 4 decimal places
				//17.2-> 172000 -> 2000
				var la:int = Math.abs((int) (latitude* 10000) % 10000);
				if(latitude<0)
					la = 10000-la;
				la = (la/1000) % 10;
				var lo:int = Math.abs((int) (longitude* 10000) % 10000);
				if(longitude<0)
					lo = 10000-lo;
				lo = (lo/1000)%10;
				
				var centiCellId:int = (la * 10) + lo;
				return Math.abs(centiCellId);
			}
		}	
		
		/**
		 * Returns the box of the given cell
		 * This may require some more work to avoid divide rounding errors
		 * @param cellId To return the lat long box of
		 * @return The box
		 */
		public static function toBoundingBoxCell(cellId:int):LatLngBounds {
			var longitude:Number = (cellId%360) - 180;
			var latitude:Number = -90;
			if (cellId>0) {
				latitude = new Number(Math.floor(cellId/360)) - 90;
			}
			return new LatLngBounds(new LatLng(latitude,longitude),new LatLng(latitude+1,longitude+1));
		}

		/**
		 * Returns the box of the given cell and centi cell
		 * An attempt has been made to avoid rounding errors with floats, but may need revisited
		 * @param cellId To return the lat long box of
		 * @param centiCellId within the box
		 * @return The box
		 */
		public static function toBoundingBoxCellAndCenti(cellId:int, centiCellId:int):LatLngBounds {
			var longitudeX10:Number = 10*((cellId%360) - 180);
			var latitudeX10:Number = -900;
			if (cellId>0) {
				latitudeX10 = 10*(new Number(Math.floor(cellId/360)) - 90);
			}
			
			var longOffset:Number = (centiCellId%10);
			var latOffset:Number = 0;
			if (centiCellId>0){
				latOffset = centiCellId/10;
			}
			
			var minLatitude:Number = (latitudeX10 + latOffset)/10;
			var minLongitude:Number = (longitudeX10 + longOffset)/10;
			var maxLatitude:Number = (latitudeX10 + latOffset + 1)/10;
			var maxlongitude:Number = (longitudeX10 + longOffset + 1)/10;
			return new LatLngBounds(new LatLng(minLatitude,minLongitude),new LatLng(maxLatitude,maxlongitude));
		}

		/**
		 * Gets the list of cells that are enclosed within the bounding box.
		 * Cells that are partially enclosed are returned also
		 * @TODO implement this properly, the current version will include cells that are partially
		 * included on the bottom and left, but not the top and right
		 * @param minLat 
		 * @param maxLat
		 * @param minLong 
		 * @param maxLong	
		 * @return The cells that are enclosed by the bounding box 
		 * @throws UnableToGenerateCellIdException if the lat longs are invalid
		 */
		public static function getCellsEnclosedBy(minLat:Number, maxLat:Number, minLong:Number, maxLong:Number):Array {
			
			if (minLat<-90) minLat=-90;
			if (maxLat>90) maxLat=90;
			if (minLong<-180) minLong=-180;
			if (maxLong>180) maxLong=180;		
			
/* 			if(logger.isDebugEnabled())
				logger.debug("Establishing cells enclosed by: " + minLat + ":" + maxLat + "   " + minLong + ":" + maxLong); */
			
			var lower:int = toCellId(minLat, minLong);
			var upper:int = toCellId(maxLat, maxLong);
			
/* 			if(logger.isDebugEnabled())
				logger.debug("Unprocessed cells: " + lower + " -> " + upper); */
			
			// if the BB upper right corner is on a grid, then it needs flagged
			if (Math.ceil(maxLong) == Math.floor(maxLong)) {
/* 				logger.debug("Longitude lies on a boundary"); */
				upper-=1;
			}
			if (Math.ceil(maxLat) == Math.floor(maxLat)) {
/* 				logger.debug("Latitude lies on a boundary"); */
				upper-=360;
			}
			
/* 			if(logger.isDebugEnabled())
				logger.debug("Getting cells contained in " + lower + " to " + upper); */
			
			var omitLeft:int = lower%360;
			var omitRight:int = upper%360;
			if(omitRight==0)
				omitRight=360;
			var cells:Array = new Array();
			for (var i:int=lower; i<=upper; i++) {
				if ((i%360 >= omitLeft
						&& i%360 <= omitRight)) {
					cells.push(i);
				}
			}
			return cells;
		}
		
		/**
		 * Return a min cell id and a max cell id for this bounding box.
		 * 
		 * @param minLongitude
		 * @param minLatitude
		 * @param maxLongitude
		 * @param maxLatitude
		 * @return the minCellId in int[0] and maxCellId in int[1]
		 */
		public static function getMinMaxCellIdsForBoundingBox(minLongitude:Number, minLatitude:Number, maxLongitude:Number, maxLatitude:Number):Array {
			
			var minCellId:int = CellIdUtils.toCellId(minLatitude, minLongitude);
			var maxCellId:int = CellIdUtils.toCellId(maxLatitude, maxLongitude);
			
			if( Math.ceil(maxLatitude)==Math.floor(maxLatitude)
					&&
				Math.ceil(maxLongitude)==Math.floor(maxLongitude)
					&& 
				(maxLongitude!=180 && maxLatitude!=90)
					&& maxCellId>0
			){
				
				//the maxLongitude,maxLatitude point is on a cell intersection, hence the maxCellId should be 
				// -361 the maxCellId CellIdUtils will give us i.e. the cell that is
				// 1 below and 1 to the left of the cell id retrieved
				//unless it is the 64799 cell.
				maxCellId = maxCellId-361; 
			}
			
			return new Array[minCellId, maxCellId];
		}
		
		/**
		 * Creates a bounding box for the list of unordered cell ids.
		 *  
		 * @param cellDensities
		 * @return a LatLongBoundingBox that encapsulates this list of cell ids.
		 */
		public static function getBoundingBoxForCells(cellIds:Array):LatLngBounds{
			if(cellIds.length < 1)
				return null;
			//first cell - gives the minLat
			//float minLatitude = toBoundingBox(cellIds.get(0)).minLat;
			//last cell - give the maxLat
			//float maxLatitude = toBoundingBox(cellIds.get(cellIds.size()-1)).maxLat;
			var minLatitudeCellId:int =cellIds[0];
			var maxLatitudeCellId:int =cellIds[cellIds.length -1];
	
			var minLongitudeCellId:int =cellIds.get(0);
			var maxLongitudeCellId:int =cellIds.get(cellIds.size()-1);
			//the min cell (id % 360) - gives min longitude
			//the max cell (id % 360) - gives max longitude
			for each (var cellId:int in cellIds){
			
				var cellIdMod360:Number = cellId % 360;
				if(cellIdMod360 < (minLongitudeCellId % 360))
					minLongitudeCellId=cellIdMod360;
				if(cellIdMod360 > (maxLongitudeCellId % 360))
					maxLongitudeCellId=cellIdMod360;
				
				if(cellId<minLatitudeCellId)
					minLatitudeCellId = cellId;
				if(cellId>maxLatitudeCellId)
					maxLatitudeCellId = cellId;
			}

			var minLongitude:Number = toBoundingBoxCell(minLongitudeCellId).southWest.lng;
			var minLatitude:Number = toBoundingBoxCell(minLatitudeCellId).southWest.lat; 
			var maxLongitude:Number = toBoundingBoxCell(maxLongitudeCellId).northEast.lng;
			var maxLatitude:Number = toBoundingBoxCell(maxLatitudeCellId).northEast.lat;
			
			return new LatLngBounds(new LatLng(minLatitude,minLongitude), new LatLng(maxLatitude, maxLongitude));
		}
		
		/**
		 * Returns the cell id and centi cell id for the supplied bounding box, Returning null if the supplied bounding box doesnt enclose a single cell.
		 * If the bounding box encloses a single cell but not a centi cell, a Integer[] of length 1 is returned with containing the cell id.
		 * Otherwise a Integer array of length 2, with Integer[0] being the cell id, Integer[1] being the centi cell.
		 *  
		 * @param minLongitude
		 * @param minLatitude
		 * @param maxLongitude
		 * @param maxLatitude
		 * @return
		 * @throws UnableToGenerateCellIdException
		 */
		public static function getCentiCellIdForBoundingBox(minLongitude:Number, minLatitude:Number, maxLongitude:Number, maxLatitude:Number):Array {
			
			var maxMinCellIds:Array = getMinMaxCellIdsForBoundingBox(minLongitude, minLatitude, maxLongitude, maxLatitude);
			if(maxMinCellIds==null || (maxMinCellIds[0]!=maxMinCellIds[1]))
				return null;
			
			var cellid:Number = maxMinCellIds[0];
			
			//ascertain whether bounding box is 0.1 by 0.1
			if(isBoundingBoxCentiCell(minLongitude, minLatitude, maxLongitude, maxLatitude)){
				
				var minCentiCell:int = toCentiCellId(minLatitude, minLongitude);
				var maxCentiCell:int = toCentiCellId(maxLatitude, maxLongitude);
		
				var maxLongitude10:Number = maxLongitude*10;
				var maxLatitude10:Number = maxLatitude*10;
				
				if( Math.ceil( maxLatitude10)==Math.floor( maxLatitude10)
						&&
					Math.ceil( maxLongitude10)==Math.floor( maxLongitude10)
						&& 
						maxCentiCell>0
				){
					
					//the maxLongitude,maxLatitude point is on a centi cell intersection, hence the maxCentiCellId should be 
					// maxCentiCellId-11 i.e. the cell that is
					// 1 below and 1 to the left of the centi cell id retrieved
					//unless it is the 100 centi cell.
					if(maxCentiCell>minCentiCell)
						maxCentiCell = maxCentiCell-11;
					else
						maxCentiCell = maxCentiCell+9;				
				}
				
				if(maxCentiCell==minCentiCell){
					return new Array[cellid, minCentiCell];
				}
			}
			return new Array[cellid];
		}		
		
		/**
		 * Undocumented
		 */ 
		private static function isBoundingBoxCentiCell(minLongitude:Number, minLatitude:Number, maxLongitude:Number, maxLatitude:Number):Boolean {
			var width:Number = maxLongitude>minLongitude ? maxLongitude-minLongitude : minLongitude-maxLongitude;
			var height:Number = maxLatitude>minLatitude ? maxLatitude-minLatitude : minLatitude-maxLatitude;
			
			return Number(height.toFixed(1))==0.1 && Number(width.toFixed(1))==0.1;
		}		
		
		
		/**
		 * Convert a CellId into a polygon
		 */
		 public static function cellIdToPolygon(cellId:int):Polygon {
		 	
		 	//convert cellId into bounding box
		 	var bbox:LatLngBounds = toBoundingBoxCell(cellId);
		 	
		 	
					
			var param:Object = new Object();
			param.points = new Array();
			param.points.push(new LatLng(bbox.northEast.lat,bbox.northEast.lng));
			param.points.push(new LatLng(bbox.northEast.lat,bbox.southWest.lng));
			param.points.push(new LatLng(bbox.southWest.lat,bbox.southWest.lng));
			param.points.push(new LatLng(bbox.southWest.lat,bbox.northEast.lng));
			param.points.push(new LatLng(bbox.northEast.lat,bbox.northEast.lng));
			param.name = "cellId "+cellId;
			
		 	return new Polygon(param, ModelLocator.getInstance().defaultPolygonStyle);
		 	
		 } 
			
	
	}
}